Skip to content

feat(accuracy): restore codegen grade concurrency (AIP-1094) - #1237

Merged
debermudez merged 28 commits into
mainfrom
dbermudez/aip-1094-restore-codegen-grade-concurrency
Aug 7, 2026
Merged

feat(accuracy): restore codegen grade concurrency (AIP-1094)#1237
debermudez merged 28 commits into
mainfrom
dbermudez/aip-1094-restore-codegen-grade-concurrency

Conversation

@debermudez

@debermudez debermudez commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the serializing asyncio.Lock in CodegenGradingWorker with an id → asyncio.Future demux table and a persistent reader task, allowing N concurrent grade_codegen() calls to run without blocking each other
  • Worker now non-blocking drains all queued stdin lines per cycle and calls codegen_metrics once with the full batch, so lighteval's ProcessPoolExecutor(max_workers=8) processes multiple problems in parallel — N concurrent grades complete in ~max(individual) instead of ~sum
  • Follow-up to fix(accuracy): grade LCB codegen in an out-of-process worker (#1145) #1175 (AIP-1089); closes AIP-1094

Changes

_codegen_worker.py

  • Added handle_batch(reqs, codegen_fn, compute_metrics_fn) — grades N requests in one codegen_metrics call, per-problem metrics demuxed from evaluate_generations' results: dict[int, list]
  • Replaced run_worker_loop with batch-drain version: blocking read of first request, then peek(0)-based drain of buffered requests, then one codegen_metrics call per cycle
  • Removed handle_request (dead code)

_codegen_worker_client.py

  • Dropped asyncio.Lock on the grade path; added _spawn_lock (spawn-only), _pending: dict[int, Future], _reader_task
  • Added _run_reader() — persistent task that reads stdout and resolves futures by id; stale ids (already timed out) are silently skipped
  • Added _mark_proven(), _dispatch_response() helpers
  • _handle_fault is idempotent (_proc is None guard); cancels all pending futures before killing
  • _kill() cancels and awaits _reader_task with self-task guard
  • aclose() uses set_exception (not cancel) so callers get CodegenWorkerError, not CancelledError
  • Removed _request()

Known tradeoff

A timeout on any in-flight grade kills the worker and faults all concurrent sibling futures with CodegenWorkerError. This is the accepted batch-coupling tradeoff documented in the issue.

Test plan

  • uv run pytest tests/unit/accuracy/ -v — new TestHandleBatch, TestRunWorkerLoopBatch, TestConcurrency classes all green
  • uv run pytest tests/component_integration/test_lcb_codegen_worker_e2e.py -v -s --run-slow — both single and concurrent e2e tests pass with pass@1 == 1.0

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance

    • Code-generation grading now processes multiple requests concurrently and in batches, improving throughput.
  • Reliability

    • Responses remain matched to the correct requests, even when completed out of order.
    • Improved handling for malformed requests, timeouts, cancellations, worker failures, and shutdowns.
    • Pending requests now receive clear errors when grading becomes unavailable.
  • Validation

    • Expanded coverage verifies concurrent grading, response matching, error recovery, batch processing, and metric handling.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

Try out this PR

Quick install:

pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@5dcfe9012ef57d1becb20341628f42a51ce4429e

Recommended with virtual environment (using uv):

uv venv --python 3.12 && source .venv/bin/activate
uv pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@5dcfe9012ef57d1becb20341628f42a51ce4429e

Last updated for commit: 5dcfe90Browse code

@github-actions github-actions Bot added the feat label Jul 31, 2026
@github-actions

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The codegen worker now batches queued requests and computes ordered per-request metrics. The client supports concurrent grading through request IDs, response demultiplexing, timeout handling, worker recovery, and safe shutdown. Unit and integration tests cover these behaviors.

Changes

Codegen concurrency

Layer / File(s) Summary
Worker batch processing
src/aiperf/accuracy/graders/_codegen_worker.py, tests/unit/accuracy/test_codegen_worker.py
The worker drains queued input, processes valid requests in one codegen_fn call, computes metrics, preserves response order, and isolates malformed or request-level errors. Tests cover batching, ordering, metric filtering, and worker-loop behavior.
Client request demultiplexing
src/aiperf/accuracy/graders/_codegen_worker_client.py, tests/unit/accuracy/test_codegen_worker_client.py
The client tracks request IDs and pending futures, dispatches responses continuously, and handles concurrent calls, stale responses, timeouts, worker failures, cancellation, and shutdown.
Concurrent grading validation
tests/component_integration/test_lcb_codegen_worker_e2e.py, .gitignore
The integration test runs four concurrent grading requests and verifies successful pass@1 results. .gitignore adds a .worktrees/ rule.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

A rabbit sends four tasks in flight,
Batches process each request right.
IDs guide answers through the night,
Stale replies vanish from sight.
Tests twitch noses: all is tight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the restoration of concurrent codegen grading, which is the main change in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/superpowers/plans/2026-07-29-codegen-grade-concurrency.md (1)

1-931: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove this plan document or move it out of version control.

This file is a step-by-step implementation plan documenting code changes and decisions for AIP-1094. The coding guidelines explicitly prohibit creating Markdown files for this purpose.

Do not commit this plan file to the repository. If the plan is useful during development, keep it outside of docs/ (e.g., in a local scratch file, a PR description, or an issue tracker) instead of committing it. If it must stay under docs/, confirm it's registered in docs/index.yml per the docs/**/*.md guideline.

Based on learnings, "**/*.md: Use Mermaid diagrams instead of ASCII art in Markdown files. Do not create Markdown files to document code changes or decisions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-29-codegen-grade-concurrency.md` around lines
1 - 931, Remove the implementation plan document from version control rather
than committing it under docs. If the plan is needed during development, move it
to an approved external location or ensure any retained docs entry is registered
in docs/index.yml, while avoiding Markdown files used solely to document code
changes or decisions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aiperf/accuracy/graders/_codegen_worker_client.py`:
- Around line 295-300: Update aclose to acquire _spawn_lock before marking
shutdown, set _closing while holding that lock, and keep the lock through the
worker termination sequence so startup cannot race with shutdown. In
_ensure_worker, check _closing after acquiring _spawn_lock and return without
spawning when shutdown has begun, including when subprocess creation is still
pending.

In `@src/aiperf/accuracy/graders/_codegen_worker.py`:
- Around line 154-213: The _drain_buffered function currently uses
stdin.peek(0), which can block after the first request is consumed. Replace this
with a non-blocking availability check on the underlying descriptor, treating
b"" and BlockingIOError as no additional data; restore blocking mode before
run_worker_loop performs the next blocking readline().

---

Outside diff comments:
In `@docs/superpowers/plans/2026-07-29-codegen-grade-concurrency.md`:
- Around line 1-931: Remove the implementation plan document from version
control rather than committing it under docs. If the plan is needed during
development, move it to an approved external location or ensure any retained
docs entry is registered in docs/index.yml, while avoiding Markdown files used
solely to document code changes or decisions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 635bfcff-db3a-45e5-a51b-e35879968cb4

📥 Commits

Reviewing files that changed from the base of the PR and between 1d18295 and 8a03966.

📒 Files selected for processing (6)
  • docs/superpowers/plans/2026-07-29-codegen-grade-concurrency.md
  • src/aiperf/accuracy/graders/_codegen_worker.py
  • src/aiperf/accuracy/graders/_codegen_worker_client.py
  • tests/component_integration/test_lcb_codegen_worker_e2e.py
  • tests/unit/accuracy/test_codegen_worker.py
  • tests/unit/accuracy/test_codegen_worker_client.py

Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py Outdated
Comment thread src/aiperf/accuracy/graders/_codegen_worker.py
@debermudez
debermudez force-pushed the dbermudez/aip-1094-restore-codegen-grade-concurrency branch from 8a03966 to 54185dc Compare July 31, 2026 20:06
@debermudez

Copy link
Copy Markdown
Contributor Author

Addressing the outside-diff comment on docs/superpowers/plans/2026-07-29-codegen-grade-concurrency.md: removed in 54185dc. The file was a dev-time artifact that shouldn't have been committed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/unit/accuracy/test_codegen_worker_client.py (1)

82-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

TestSerialization no longer tests serialization; consider moving this test into TestConcurrency.

The comment at Line 83-84 states this class previously verified serialized execution and now verifies the opposite (concurrent execution without a lock). Keeping the old class name is confusing since a TestConcurrency class already exists immediately below with the same concurrency scope. Move test_concurrent_grades_return_correct_results into TestConcurrency and drop the now-empty TestSerialization class, or rename the class to reflect its new purpose.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/accuracy/test_codegen_worker_client.py` around lines 82 - 95, Move
test_concurrent_grades_return_correct_results from TestSerialization into the
existing TestConcurrency class, preserving its assertions and cleanup. Remove
the now-empty TestSerialization class and update any obsolete serialization
wording associated with the test.
src/aiperf/accuracy/graders/_codegen_worker.py (1)

80-158: 🚀 Performance & Scalability | 🔵 Trivial

handle_batch correctly isolates per-request failures and preserves order.

The malformed-JSON, non-dict, missing-field, batch-exception, and per-item-metric-exception paths all populate responses at the original index and are covered by the referenced unit tests (test_malformed_request_in_batch_does_not_affect_others, test_batch_exception_returns_error_for_all, test_response_order_matches_request_order). The blind except Exception at Line 134 and Line 151 flagged by Ruff is intentional per the docstring's "Never raises" guarantee, so isolated per-request/per-batch failures do not take down the worker loop.

One scalability note: _drain_buffered (called from run_worker_loop) can grow reqs unboundedly while queued input exists, and no response is written until the entire batch's codegen_fn call completes. Under a burst of many concurrent grade_codegen calls, this can inflate per-request latency and peak memory for large evaluation_sample/generated_code payloads. Consider capping batch size if this becomes an issue in practice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/accuracy/graders/_codegen_worker.py` around lines 80 - 158,
Consider adding a maximum batch-size limit to the _drain_buffered flow used by
run_worker_loop so reqs cannot grow without bound while input remains queued.
Process buffered requests in capped batches and preserve response ordering and
existing handle_batch error isolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aiperf/accuracy/graders/_codegen_worker.py`:
- Around line 161-204: The non-blocking loop in _drain_buffered must not submit
incomplete JSONL fragments. Before calling stdin.readline(), verify the bytes
returned by stdin.peek(0) contain b"\n"; if not, stop draining so blocking mode
is restored and the next run_worker_loop cycle can complete the line.

In `@tests/unit/accuracy/test_codegen_worker_client.py`:
- Around line 155-169: Update test_stale_id_after_timeout_does_not_crash to
issue the documented second grade_codegen call after the expected timeout, using
a real timeout and the existing worker setup. Preserve the initial
near-zero-timeout assertion, and ensure the follow-up request verifies the
worker remains usable without hanging or crashing.

---

Nitpick comments:
In `@src/aiperf/accuracy/graders/_codegen_worker.py`:
- Around line 80-158: Consider adding a maximum batch-size limit to the
_drain_buffered flow used by run_worker_loop so reqs cannot grow without bound
while input remains queued. Process buffered requests in capped batches and
preserve response ordering and existing handle_batch error isolation.

In `@tests/unit/accuracy/test_codegen_worker_client.py`:
- Around line 82-95: Move test_concurrent_grades_return_correct_results from
TestSerialization into the existing TestConcurrency class, preserving its
assertions and cleanup. Remove the now-empty TestSerialization class and update
any obsolete serialization wording associated with the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 3fe99340-813b-4bf1-b636-3f7ca8643ed7

📥 Commits

Reviewing files that changed from the base of the PR and between 8a03966 and 54185dc.

📒 Files selected for processing (5)
  • src/aiperf/accuracy/graders/_codegen_worker.py
  • src/aiperf/accuracy/graders/_codegen_worker_client.py
  • tests/component_integration/test_lcb_codegen_worker_e2e.py
  • tests/unit/accuracy/test_codegen_worker.py
  • tests/unit/accuracy/test_codegen_worker_client.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/component_integration/test_lcb_codegen_worker_e2e.py
  • src/aiperf/accuracy/graders/_codegen_worker_client.py
  • tests/unit/accuracy/test_codegen_worker.py

Comment thread src/aiperf/accuracy/graders/_codegen_worker.py
Comment thread tests/unit/accuracy/test_codegen_worker_client.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/aiperf/accuracy/graders/_codegen_worker.py (1)

191-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a pipe-backed partial-line test.

Use io.BufferedReader over os.pipe(). Write one JSONL request in two writes and assert that the request completes after the newline arrives. Existing BytesIO tests do not exercise peek(0) or the partial-line guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/accuracy/graders/_codegen_worker.py` around lines 191 - 196, Add a
pipe-backed partial-line test for the worker’s JSONL request-reading path, using
io.BufferedReader over os.pipe(). Split one request across two writes, verify
the first partial write does not complete processing, then write the newline and
assert the request completes successfully; keep the test focused on exercising
peek(0) and the partial-line guard around the worker read loop.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/aiperf/accuracy/graders/_codegen_worker.py`:
- Around line 191-196: Add a pipe-backed partial-line test for the worker’s
JSONL request-reading path, using io.BufferedReader over os.pipe(). Split one
request across two writes, verify the first partial write does not complete
processing, then write the newline and assert the request completes
successfully; keep the test focused on exercising peek(0) and the partial-line
guard around the worker read loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2b8b50ac-1a2f-42d9-b2fa-8d3f1d97c797

📥 Commits

Reviewing files that changed from the base of the PR and between 54185dc and 2e9f6ca.

📒 Files selected for processing (2)
  • src/aiperf/accuracy/graders/_codegen_worker.py
  • tests/unit/accuracy/test_codegen_worker_client.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/accuracy/test_codegen_worker_client.py

@debermudez
debermudez force-pushed the dbermudez/aip-1094-restore-codegen-grade-concurrency branch from 2e9f6ca to b7f4edc Compare July 31, 2026 21:59
@debermudez

Copy link
Copy Markdown
Contributor Author

Fixed in b7f4edc — added TestRunWorkerLoopBatch.test_partial_jsonl_line_is_deferred_to_next_cycle which uses a real os.pipe() + BufferedReader, writes the request body without a trailing newline, asserts nothing is emitted, then writes the newline and asserts the response arrives. Exercises the O_NONBLOCK + partial-line guard path that BytesIO tests can't reach.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (7)
tests/unit/accuracy/test_codegen_worker.py (4)

135-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the existing batch fixture instead of redefining it.

_nan_inf is identical to _fake_codegen_batch_ok at lines 32-38. Pass _fake_codegen_batch_ok and keep only the custom _nan_compute.

♻️ Proposed simplification
     def test_non_finite_metric_values_are_dropped(self) -> None:
         # NaN/Inf must not cross the JSONL boundary (repo NaN/Inf discipline).
-        def _nan_inf(
-            samples: list, generations: list, **_kwargs: Any
-        ) -> tuple[dict[str, Any], dict[int, list]]:
-            n = len(samples)
-            return {"pass@1": 1.0}, {i: [[True]] for i in range(n)}
-
         def _nan_compute(results: dict, **_kwargs: Any) -> dict[str, Any]:
             return {"pass@1": float("nan"), "extra": float("inf"), "ok": 1.0}
 
         req = {"id": 9, "evaluation_sample": [{}], "generated_code": [["x"]]}
-        resps = worker.handle_batch([req], _nan_inf, _nan_compute)
+        resps = worker.handle_batch([req], _fake_codegen_batch_ok, _nan_compute)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/accuracy/test_codegen_worker.py` around lines 135 - 147, Update
test_non_finite_metric_values_are_dropped to remove the duplicate _nan_inf
helper and pass the existing _fake_codegen_batch_ok fixture to
worker.handle_batch, retaining only the custom _nan_compute behavior.

202-203: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move threading and time imports to the file top.

The repository test guidelines require imports at the file top.

As per coding guidelines: "keep imports at the top".

♻️ Proposed change
     def test_partial_jsonl_line_is_deferred_to_next_cycle(self) -> None:
         # Exercises the O_NONBLOCK peek(0) + partial-line guard in _drain_buffered.
         # A partial write (no trailing newline) must not be submitted as a request;
         # only after the newline arrives should the line be processed.
-        import threading
-        import time
-
         req = self._req(42)

Add at the file top:

import threading
import time
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/accuracy/test_codegen_worker.py` around lines 202 - 203, Move the
threading and time imports from their local position near the affected test into
the module-level import section at the top of
tests/unit/accuracy/test_codegen_worker.py, leaving their usage unchanged.

Source: Coding guidelines


241-244: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Annotate the codegen_fn parameter.

codegen_fn has no type hint. TestRunWorkerLoopBatch._run already uses Callable[..., tuple[dict[str, Any], dict[int, list]]]. Use the same annotation here.

As per coding guidelines: "Add type hints to every function parameter and return value".

♻️ Proposed change
-    def _run(self, requests: list[bytes], codegen_fn) -> list[dict]:
+    def _run(
+        self,
+        requests: list[bytes],
+        codegen_fn: Callable[..., tuple[dict[str, Any], dict[int, list]]],
+    ) -> list[dict[str, Any]]:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/accuracy/test_codegen_worker.py` around lines 241 - 244, Update
the _run method’s codegen_fn parameter annotation to match
TestRunWorkerLoopBatch._run: Callable[..., tuple[dict[str, Any], dict[int,
list]]]. Preserve the existing requests annotation and list[dict] return
annotation.

Source: Coding guidelines


210-237: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the shared BytesIO against cross-thread access.

The worker thread writes to out while the main thread calls out.seek(0) and out.read() at lines 224-225. io.BytesIO keeps one shared file position, so a concurrent write would land at the position set by the main thread. The current ordering is safe because the worker writes only after line 228, but the pattern breaks silently if the timing changes. Consider a small lock-protected writer wrapper, or snapshot with out.getvalue() instead of seek/read.

♻️ Proposed change
-            assert t.is_alive()
-            out.seek(0)
-            assert out.read() == b""  # nothing written yet
+            assert t.is_alive()
+            assert out.getvalue() == b""  # nothing written yet
@@
         assert not t.is_alive()
-        out.seek(0)
-        resps = [orjson.loads(ln) for ln in out if ln.strip()]
+        resps = [
+            orjson.loads(ln) for ln in out.getvalue().splitlines() if ln.strip()
+        ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/accuracy/test_codegen_worker.py` around lines 210 - 237, Protect
the shared BytesIO access in the worker-thread test around the output inspection
after starting worker.run_worker_loop. Replace the main thread’s seek/read
pattern with a position-independent snapshot such as getvalue(), and use that
snapshot for both the empty-output assertion and final response parsing so
concurrent writes cannot alter the shared file position.
.gitignore (1)

60-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicate ignore rule.

Line 56 already ignores .worktrees/.

♻️ Proposed change
 tests/scripts/.chaos_runs/
-.worktrees/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore at line 60, Remove the duplicate `.worktrees/` entry from the
changed section, keeping the existing ignore rule at line 56 as the sole rule.
src/aiperf/accuracy/graders/_codegen_worker_client.py (2)

296-300: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the pending-future failure loop.

The same loop appears in _handle_fault at lines 251-254. Extract a small helper so both paths stay consistent.

♻️ Proposed change
+    def _fail_pending(self, message: str) -> None:
+        for fut in list(self._pending.values()):
+            if not fut.done():
+                fut.set_exception(CodegenWorkerError(message))
+        self._pending.clear()
+
     async def aclose(self) -> None:
-        for fut in list(self._pending.values()):
-            if not fut.done():
-                fut.set_exception(CodegenWorkerError("grading worker closed"))
-        self._pending.clear()
+        self._fail_pending("grading worker closed")
         await self._kill()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/accuracy/graders/_codegen_worker_client.py` around lines 296 -
300, Extract the pending-future failure loop from the current close path into a
small helper near the relevant methods, then call that helper from both the
close flow and `_handle_fault`. Preserve the existing behavior of setting
CodegenWorkerError("grading worker closed") on unfinished futures and clearing
`_pending` exactly once.

218-240: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fault the worker if the reader raises an unexpected exception.

The try block only handles ValueError, ConnectionError, BrokenPipeError, orjson.JSONDecodeError, and CancelledError. Any other exception ends the reader task silently. Every pending caller then waits for its full timeout, and the task exception is only reported when the task is garbage collected.

Add a final except Exception that calls _handle_fault(). Note that BrokenPipeError is a subclass of ConnectionError, so it is redundant in the tuple at line 222.

🛠️ Proposed change
         except asyncio.CancelledError:
             pass
+        except Exception:
+            # A reader crash would leave every pending caller waiting for its
+            # timeout, so convert it into a worker fault.
+            await self._handle_fault()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/accuracy/graders/_codegen_worker_client.py` around lines 218 -
240, Update the reader loop in the worker client to catch unexpected exceptions
with a final except Exception branch and await _handle_fault() before returning
or completing. Remove the redundant BrokenPipeError entry from the existing
ConnectionError tuple, while preserving the current CancelledError handling and
fault behavior for known read, parse, and dispatch failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aiperf/accuracy/graders/_codegen_worker_client.py`:
- Around line 120-124: Update _ensure_worker to cancel and await the existing
_reader_task and _stderr_task before spawning a replacement whenever the current
process has exited, then clear the old worker task/process state as appropriate
before resetting _worker_proven. Ensure the respawn path cannot let stale
readers invoke _handle_fault against the new worker.
- Around line 193-200: Update the response handling around _pending.pop in the
worker client to detect resp values with id equal to None before treating the
response as stale, and return the protocol-fault result so callers fail fast.
Preserve existing handling for valid pending IDs, cancelled futures, and
unhashable IDs.
- Around line 115-118: Update the CancelledError handling in the request flow to
remove the cancelled request from _pending and re-raise the cancellation without
calling _handle_fault. Preserve worker usability so concurrent requests
continue, relying on _dispatch_response to discard any late response for the
stale request ID.
- Around line 106-108: Update the request-writing flow around the worker
client’s stdin write to explicitly validate that the process and stdin are
available instead of relying on assert, and catch BrokenPipeError and
ConnectionResetError from write or drain. Route these failures through
_handle_fault so the caller receives CodegenWorkerError and the pending
entry/future is cleaned up.

In `@src/aiperf/accuracy/graders/_codegen_worker.py`:
- Around line 113-122: Update the request-processing block around all_samples
and all_generations to read both evaluation_sample and generated_code into local
values before modifying any batch lists. Append to all_samples, all_generations,
and id_map only after both lookups succeed, preserving aligned entries when
malformed requests are caught by the existing exception handler.
- Around line 205-210: Update the stdin-reading fallback around the fd/`fcntl`
selection to run the read-all `stdin.read()` loop only when `fd < 0`, matching
the intended in-memory `BytesIO` path. When a real file descriptor exists but
`fcntl` is unavailable, skip the drain instead of blocking on the pipe, while
preserving the existing nonblocking read behavior when `fcntl` is available.

---

Nitpick comments:
In @.gitignore:
- Line 60: Remove the duplicate `.worktrees/` entry from the changed section,
keeping the existing ignore rule at line 56 as the sole rule.

In `@src/aiperf/accuracy/graders/_codegen_worker_client.py`:
- Around line 296-300: Extract the pending-future failure loop from the current
close path into a small helper near the relevant methods, then call that helper
from both the close flow and `_handle_fault`. Preserve the existing behavior of
setting CodegenWorkerError("grading worker closed") on unfinished futures and
clearing `_pending` exactly once.
- Around line 218-240: Update the reader loop in the worker client to catch
unexpected exceptions with a final except Exception branch and await
_handle_fault() before returning or completing. Remove the redundant
BrokenPipeError entry from the existing ConnectionError tuple, while preserving
the current CancelledError handling and fault behavior for known read, parse,
and dispatch failures.

In `@tests/unit/accuracy/test_codegen_worker.py`:
- Around line 135-147: Update test_non_finite_metric_values_are_dropped to
remove the duplicate _nan_inf helper and pass the existing
_fake_codegen_batch_ok fixture to worker.handle_batch, retaining only the custom
_nan_compute behavior.
- Around line 202-203: Move the threading and time imports from their local
position near the affected test into the module-level import section at the top
of tests/unit/accuracy/test_codegen_worker.py, leaving their usage unchanged.
- Around line 241-244: Update the _run method’s codegen_fn parameter annotation
to match TestRunWorkerLoopBatch._run: Callable[..., tuple[dict[str, Any],
dict[int, list]]]. Preserve the existing requests annotation and list[dict]
return annotation.
- Around line 210-237: Protect the shared BytesIO access in the worker-thread
test around the output inspection after starting worker.run_worker_loop. Replace
the main thread’s seek/read pattern with a position-independent snapshot such as
getvalue(), and use that snapshot for both the empty-output assertion and final
response parsing so concurrent writes cannot alter the shared file position.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 740b1553-a479-40e9-a264-8debc208ec7e

📥 Commits

Reviewing files that changed from the base of the PR and between 2e9f6ca and b7f4edc.

📒 Files selected for processing (6)
  • .gitignore
  • src/aiperf/accuracy/graders/_codegen_worker.py
  • src/aiperf/accuracy/graders/_codegen_worker_client.py
  • tests/component_integration/test_lcb_codegen_worker_e2e.py
  • tests/unit/accuracy/test_codegen_worker.py
  • tests/unit/accuracy/test_codegen_worker_client.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/component_integration/test_lcb_codegen_worker_e2e.py
  • tests/unit/accuracy/test_codegen_worker_client.py

Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py Outdated
Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py
Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py
Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py
Comment thread src/aiperf/accuracy/graders/_codegen_worker.py
Comment thread src/aiperf/accuracy/graders/_codegen_worker.py Outdated
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 27 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../aiperf/accuracy/graders/_codegen_worker_client.py 83.20% 17 Missing and 5 partials ⚠️
src/aiperf/accuracy/graders/_codegen_worker.py 94.11% 4 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/aiperf/accuracy/graders/_codegen_worker_client.py (1)

293-296: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Kill the process group after the worker leader exits.

Line 293 skips _kill_process_group() when proc.returncode is already set. The worker can exit before a lighteval sandbox child exits. That child remains in the dedicated process group while _ensure_worker() spawns a replacement.

Invoke process-group termination whenever proc exists. Treat an already-gone process group as successful cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiperf/accuracy/graders/_codegen_worker_client.py` around lines 293 -
296, Update the cleanup block around _kill_process_group in _ensure_worker so
process-group termination runs whenever proc exists, regardless of
proc.returncode. Preserve suppressing ProcessLookupError and awaiting the
process as needed, treating an already-gone group as successful cleanup before
spawning a replacement.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aiperf/accuracy/graders/_codegen_worker_client.py`:
- Around line 110-129: Update the request flow around the stdin write and
response wait so the single timeout covers both `proc.stdin.drain()` and
`asyncio.wait_for(fut, ...)`. Establish one deadline before draining, use the
remaining time for the response await, and ensure `asyncio.CancelledError`
removes `req_id` from `_pending` whether cancellation happens during drain or
response handling; preserve timeout fault handling and error propagation.

---

Outside diff comments:
In `@src/aiperf/accuracy/graders/_codegen_worker_client.py`:
- Around line 293-296: Update the cleanup block around _kill_process_group in
_ensure_worker so process-group termination runs whenever proc exists,
regardless of proc.returncode. Preserve suppressing ProcessLookupError and
awaiting the process as needed, treating an already-gone group as successful
cleanup before spawning a replacement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fcc96d53-7355-4faa-86c7-d3d3ecdb1235

📥 Commits

Reviewing files that changed from the base of the PR and between b7f4edc and 6e8240e.

📒 Files selected for processing (3)
  • src/aiperf/accuracy/graders/_codegen_worker.py
  • src/aiperf/accuracy/graders/_codegen_worker_client.py
  • tests/unit/accuracy/test_codegen_worker_client.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/aiperf/accuracy/graders/_codegen_worker.py
  • tests/unit/accuracy/test_codegen_worker_client.py

Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py
@debermudez

Copy link
Copy Markdown
Contributor Author

Addressing the two comments from the latest review:

Kill process group after worker exits (lines 293-296): Fixed in 49ab2f5_kill() now calls _kill_process_group() regardless of proc.returncode, so lighteval's forked sandbox grandchildren are reaped even when the worker leader has already exited naturally. ProcessLookupError (group already gone) is suppressed by the existing handler in _kill_process_group. await proc.wait() only runs when returncode is None to avoid waiting on an already-reaped process.

Single deadline covering drain + response wait (lines 110-129): Not addressing in this PR. The current split (drain is unbudgeted, wait_for(fut, timeout) covers the response) is correct and safe — in practice drain() completes immediately for small JSONL payloads. Restructuring around a shared asyncio.timeout() deadline would change error-handling semantics across drain and response paths and is out of scope for this concurrency PR.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…ustness

- Replace _drain_fd (broken: select() unreliable after BufferedReader pulls
  kernel data into userspace) and _drain_seekable with _drain_buffered that
  uses peek() for BufferedReader and a seekable fallback for BytesIO in tests.
  Remove unused _parse_batch and import select.
- aclose() now sets CodegenWorkerError on pending futures instead of calling
  cancel(), so shutdown does not propagate CancelledError to grader callers
  that only catch CodegenWorkerError.
- Extract _dispatch_response helper so _run_reader complexity stays within
  the C901 limit; unhashable req_id from a desynced worker now triggers a
  fault instead of killing the reader task with TypeError.
- grade_codegen calls await stdin.drain() after write() to respect backpressure.
- Add test_list_shaped_pass_at_1_is_preserved to TestHandleBatch (guards the
  silent-0.000 bug for list-shaped pass@1 from lighteval).
- Remove unused _ECHO_ID_IN_METRICS constant from client tests.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…remove plan doc

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…test

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…e-backed partial-line drain test

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…, and batch alignment

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…h in drain

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
… exited

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…g-vs-exception race

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
@debermudez
debermudez force-pushed the dbermudez/aip-1094-restore-codegen-grade-concurrency branch from d197aab to 98786ac Compare August 3, 2026 19:08
@debermudez
debermudez marked this pull request as ready for review August 3, 2026 21:06

@ajcasagrande ajcasagrande left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The design here is right — lighteval builds a fresh ProcessPoolExecutor per evaluate_generations call, so collapsing N requests into one codegen_metrics call is exactly the correct lever, and the id→future demux is clean. But there is one release-blocking defect that makes the whole LiveCodeBench benchmark report zeros, plus a dead except branch from the last commit.

Blocking

handle_batch nests the payload one level too deep, so every LCB grade silently returns pass@1 = 0.0 with ok: true. CodeExecutionGrader already sends lighteval's list form ([{...}] / [[code]]); origin/main forwarded that straight through, the PR wraps it again with .append(). lighteval's evaluate_generations then hands check_correctness a list where it expects a dict, run_test dies on sample["input_output"] with TypeError: list indices must be integers or slices, not str, and lighteval's own except Exception: pass swallows it into the [-2] "compile error" sentinel. No error surfaces anywhere.

Verified against the real worker subprocess and real lighteval 0.13.0, same request, same spawn parameters:

[PR #1237]    {'id': 1, 'ok': True, 'metrics': {'pass@1': 0.0}}
[origin/main] {'id': 1, 'ok': True, 'metrics': {'pass@1': 1.0}}

Note the shape of the failure: wrong answers still "pass" because their expected value is already 0.0. Only a known-correct solution exposes it — which no concurrent test in this PR uses.

Why CI is green anyway

Three independent gaps, all worth closing regardless of the fix:

  1. _fake_codegen_batch_ok only reads len(samples) and never dereferences an element, so TestHandleBatch passes identically with and without the bug. I confirmed the unit suite is green on this HEAD and with the fix applied.
  2. test_worker_grades_multiple_problems_concurrently grades 4 identical, all-correct problems, so every expected value is equal and misalignment is invisible.
  3. Both e2e tests are @pytest.mark.slow, and run-unit-tests.yml runs component_integration with -m 'not ... and not slow', so CI never executes them.

Suggested fix order

  1. Flatten the batch payload (blocking) — a ~6-line change; I verified it restores 6/6 correct verdicts on distinct mixed-outcome problems.
  2. Add a distinct-problem concurrent test that would have caught this, and get one real-lighteval batch test into CI.
  3. Reorder the except clauses so the drain-timeout branch is reachable.
  4. Decide on retrying faulted siblings, or document the measured blast radius.
  5. Cleanups (assert escaping aclose(), the no-op respawn test, stale line refs).

What's working well

  • Per-problem demux is correct once the nesting is fixed — verified 6/6 on distinct problems with mixed pass/fail.
  • _next_id stays monotonic across respawns, so a late response from a killed worker can never collide with a new id.
  • The _kill() self-task guard correctly avoids the self-await deadlock when the reader detects its own fault.
  • Draining stderr before unblocking callers is a genuinely subtle ordering fix and the comment earns its place.
  • Protocol-fd isolation from forked sandbox children is careful and well justified.

Separate, pre-existing (not this PR)

_ensure_worker always sets AIPERF_CODEGEN_DEATH_FD, which starts the worker's watcher thread — despite the module docstring promising "a fresh, single-threaded interpreter" and the whole point of #1145 being to keep lighteval's fork away from a multithreaded parent. With that thread present, lighteval grades a known-correct solution 0.0 on origin/main too. Identical on both branches, so not a regression from this PR, but it means these e2e tests are red on main as well. Probably deserves its own issue.

Reproduction scripts and logs for every claim above are available on request; each finding was validated against the real worker + lighteval 0.13.0 on CPython 3.12.10.

Comment thread src/aiperf/accuracy/graders/_codegen_worker.py Outdated
Comment thread src/aiperf/accuracy/graders/_codegen_worker.py Outdated
Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py
Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py
Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py Outdated
Comment thread tests/unit/accuracy/test_codegen_worker.py
Comment thread tests/component_integration/test_lcb_codegen_worker_e2e.py Outdated
Comment thread tests/unit/accuracy/test_codegen_worker_client.py
@ajcasagrande

Copy link
Copy Markdown
Contributor

Definitive proof + root cause for the blocking finding

Following up on my review with an end-to-end reproduction through the real aiperf profile CLI, plus the exact cause.

Cause: one .append()

CodeExecutionGrader already builds lighteval's list form (code_execution.py:139,144):

evaluation_sample = _build_evaluation_sample(...)   # -> [ {"input_output": ...} ]
generated_code    = [[snippet]]                     # -> [ [str] ]

origin/main forwarded those straight through as samples_list / generations_list. This PR wraps them again (_codegen_worker.py:123-125):

all_samples.append(sample)          # sample IS [ {...} ]  ->  [ [ {...} ] ]
all_generations.append(generation)

lighteval then indexes per problem (codegen_metrics.py:572-573):

inputs = [[(generations_list[index], samples_list[index], timeout), index] ...]

so check_correctness receives a list where it expects the sample dict, and run_test dies at codegen_metrics.py:365 on sample["input_output"]:

TypeError: list indices must be integers or slices, not str

That exception is swallowed by lighteval's own evaluate_generations_by_problem (except Exception: pass), leaving the [-2] "compile error" sentinel. The worker therefore returns ok: true with pass@1: 0.0 — no exception, no log line, unparsed = 0. Every LiveCodeBench problem scores zero and the run looks healthy. That is the same failure mode issue #1145 existed to fix.

Note the shape of it: a wrong answer still "passes" its check, because its expected score is already 0.0. Only a known-correct solution exposes the bug — which no concurrent test here uses.

Proof

Four independent levels, all against real lighteval 0.13.0 (no stubs):

level origin/main this PR
one request, real worker subprocess, identical spawn args pass@1 = 1.0 pass@1 = 0.0
6 distinct problems in one drained batch correct mixed verdicts all 0.0
full aiperf profile CLI (below) 4/6 0/6
tests/unit/accuracy/ pass pass — catches nothing

The CLI run. Real lcb-codegeneration benchmark against the mock server's accuracy-oracle mode. The oracle is 6 real LCB problems from the v4_v5 subset, each answered by a lookup-table program mapping every test case's stdin to its expected stdout — so a served-correct row genuinely executes to pass@1 = 1.0. All 6 rows are correct solutions; the mock decides which come back wrong, via --random-seed 42 --accuracy-correct-rate 0.5. That decision is seeded per prompt, so it is order-independent and identical across arms — which makes the mock's own tally an independent oracle.

Only _codegen_worker.py and _codegen_worker_client.py differ between arms. Fresh mock per arm.

================ origin/main ================
mock  /accuracy : matched 6, correct 4, incorrect 2, unmatched 0
aiperf CSV      : OVERALL,4,6,0,0.6667
CROSS-CHECK     : mock served correct 4/6 | aiperf graded correct 4/6  -> MATCH

================ this PR ====================
mock  /accuracy : matched 6, correct 4, incorrect 2, unmatched 0
aiperf CSV      : OVERALL,0,6,0,0.0000
CROSS-CHECK     : mock served correct 4/6 | aiperf graded correct 0/6  -> MISMATCH
                  (4 known-correct answers graded wrong, unparsed=0)

origin/main agrees with the oracle exactly. This branch disagrees by precisely the 4 correct answers, silently.

Fix

Flatten instead of nesting, and track each request's span so the per-problem demux still holds:

id_map: list[tuple[int, Any, int, int]] = []   # (req_idx, req_id, start, count)
...
start = len(all_samples)
all_samples.extend(sample)
all_generations.extend(generation)
id_map.append((i, req_id, start, len(all_samples) - start))
...
for req_idx, req_id, start, count in id_map:
    metrics = compute_metrics_fn(
        {j: raw_results[start + j] for j in range(count)},
        k_list=list(_LCB_PASS_AT_K),
    )

With this applied I get 6/6 correct verdicts on 6 distinct mixed-outcome problems in a shared batch, and tests/unit/accuracy/ stays green. The rest of the design is sound — batching into one codegen_metrics call is the right lever (lighteval builds a fresh ProcessPoolExecutor per call), and the raw_results indexing is otherwise correct.

Correction to my earlier comment

In the review summary I said the pre-existing AIPERF_CODEGEN_DEATH_FD problem was caused by the watcher thread making the worker multithreaded. That was wrong — a daemon thread blocked in os.read is harmless here. A 2×2×2 bisect over {stdout-guard, at-fork close, thread} shows only guard + atfork fails:

CASE=guard                 pass@1=1.0
CASE=atfork                pass@1=1.0
CASE=thread                pass@1=1.0
CASE=guard+atfork          pass@1=0.0  raw={0: [[-1]]}   <-- culprit
CASE=guard+atfork+thread   pass@1=0.0  raw={0: [[-1]]}

_install_stdout_guard and _start_death_watcher each register an os.register_at_fork(after_in_child=close(fd)). Both re-fire at every lighteval fork (ProcessPoolExecutor worker → multiprocessing.Manager()mp.Process), closing fd numbers that multiprocessing may since have recycled for its own pipes. [[-1]] is lighteval's "result list empty" path — the forked child died.

This reproduces identically on origin/main, so it is pre-existing and out of scope for this PR (probably deserves its own issue). It does mean both e2e tests in test_lcb_codegen_worker_e2e.py are red on main too — they're slow-marked, so CI never runs them. I disabled that path identically in both arms above, which is why origin/main scores 66.67% rather than 0%.

@ajcasagrande

Copy link
Copy Markdown
Contributor

Pre-existing: the worker grades every LCB problem 0.0 on main (from #1175)

Separate from the batching bug in this PR, and not introduced by it — but it is the same worker, and it comes from your #1175, so flagging it here rather than in isolation. It also explains why the two e2e tests in this PR's test plan cannot pass as written.

On main, the LCB codegen worker grades every problem pass@1 = 0.0 whenever the client spawns it — which is always, since CodegenGradingWorker._ensure_worker unconditionally sets AIPERF_CODEGEN_DEATH_FD. A known-correct solution scores 0, with ok: true, no exception, no log line, and unparsed = 0 in the accuracy export.

This is the same symptom #1145 was filed to fix, reintroduced by the fix itself (817a8d84d, #1175). Independent of the batching bug in this PR — I found it while reviewing.

Root cause: two register_at_fork handlers closing recycled fd numbers

_codegen_worker.py registers two at-fork handlers:

_install_stdout_guard:  os.register_at_fork(after_in_child=lambda: _close_fd_quietly(protocol_fd))
_start_death_watcher:   os.register_at_fork(after_in_child=lambda: _close_fd_quietly(death_fd))

Each closes a raw fd number, and both re-fire at every subsequent fork. lighteval forks repeatedly per grade:

worker → ProcessPoolExecutor worker → multiprocessing.Manager() → mp.Process(_temp_run)

After the first fork closes fds N and M in the child, those numbers are free, and multiprocessing promptly reuses them for its own pipes and manager sockets. At the next fork down the chain the same handlers fire again and close the recycled descriptors, so _temp_run dies before appending a result. check_correctness then hits its empty-result path and returns the [-1] sentinel (codegen_metrics.py:513-516), which becomes pass@1 = 0.0.

Evidence

2×2×2 bisect over {stdout-guard, at-fork close, watcher thread}, in-process, one known-correct stdin/stdout solution:

CASE=none                  pass@1=1.0  raw={0: [[True]]}
CASE=guard                 pass@1=1.0  raw={0: [[True]]}
CASE=atfork                pass@1=1.0  raw={0: [[True]]}
CASE=thread                pass@1=1.0  raw={0: [[True]]}
CASE=guard+atfork          pass@1=0.0  raw={0: [[-1]]}   <-- culprit
CASE=guard+thread          pass@1=1.0  raw={0: [[True]]}
CASE=atfork+thread         pass@1=1.0  raw={0: [[True]]}
CASE=guard+atfork+thread   pass@1=0.0  raw={0: [[-1]]}

Only the pair fails. The watcher thread is not the cause — a daemon thread blocked in os.read is harmless here.

At the worker-process level, toggling only the env var:

worker, AIPERF_CODEGEN_DEATH_FD unset -> {"id":1,"ok":true,"metrics":{"pass@1":1.0}}
worker, AIPERF_CODEGEN_DEATH_FD set   -> {"id":1,"ok":true,"metrics":{"pass@1":0.0}}

Impact

  • Any --accuracy-benchmark lcb-codegeneration run reports ~0% regardless of model quality.
  • Silent: ok: true, unparsed = 0, no error surfaces to the grader or the accuracy report.
  • Both e2e tests in tests/component_integration/test_lcb_codegen_worker_e2e.py fail on main. They are @pytest.mark.slow, and run-unit-tests.yml runs component_integration with -m 'not ... and not slow', so CI never executes them.

Reproduction

uv pip install -e ".[dev,accuracy]"
uv run pytest tests/component_integration/test_lcb_codegen_worker_e2e.py \
    -v -m "component_integration and slow"
# both fail: assert 0.0 == 1.0

(Environment: CPython 3.12.10, lighteval 0.13.0, Linux.)

Suggested fix

Make each handler one-shot per process, so a nested fork can never close a recycled fd number:

def _close_once(holder: list[int | None]) -> None:
    fd = holder[0]
    if fd is None:
        return
    holder[0] = None          # never close this number again in this process
    _close_fd_quietly(fd)

_protocol_holder: list[int | None] = [protocol_fd]
os.register_at_fork(after_in_child=lambda: _close_once(_protocol_holder))

Verified against the same harness:

CASE=buggy        pass@1=0.0  raw={0: [[-1]]}
CASE=oneshot      pass@1=1.0  raw={0: [[True]]}

The intent of both handlers — keeping the protocol fd and death fd out of untrusted sandbox children — is preserved: the first fork still closes them, and the child no longer holds either descriptor.

Worth pairing with a CI lane that runs -m slow (or dropping the marker on one real-lighteval test), since nothing currently exercises this path.

…1=0.0

evaluation_sample and generated_code arrive as lists (e.g.
[{"input_output": "..."}] and [["code"]]) — appending them wrapped each
request in an extra list layer, making lighteval index a list where it
expected a dict and silently returning pass@1=0.0 for every grade.

Switch to extend() and track (start, count) per request in id_map so
the demux loop addresses raw_results[start+j] correctly. This is safe
for the current single-problem-per-request contract and also correct if
a request ever carries multiple samples.

Fixes thread #3708795679 and #3708795685.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
TimeoutError is an OSError subclass (PEP 3151), so the previous
except (OSError, ConnectionError) clause swallowed drain timeouts before
the except TimeoutError branch ran. _handle_fault() therefore never
executed on a stuck-stdin worker, leaving it alive to stall every
subsequent grade until the response-wait timeout eventually killed it.

Reorder to catch TimeoutError first.

Fixes thread #3708795687 and closes #3693675461.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
_kill() sets self._proc = None before awaiting proc.wait(). If the
reader task is scheduled for the first time during that await, the
previous assert self._proc is not None fired and propagated an
AssertionError out of _kill() -> aclose(). Replace with an early return.

Fixes thread #3708795694.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
The previous fake only read len(samples) and never dereferenced elements,
so the double-nesting bug (append vs extend) passed green. Add an element-
type assertion matching lighteval's contract so the fake is load-bearing.

Fixes thread #3708795696.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
All four grades used the same sample and correct solution, so every
expected pass@1 was 1.0. Swapped, duplicated, or misattributed results
would all satisfy the assertion — it could only detect "all wrong", not
"wrong per problem".

Replace with four problems with different expected verdicts (three
correct, one deliberately wrong) so per-problem batching misalignment
is detectable.

Fixes thread #3708795698.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…ions

The previous test had no assertions and drove respawn by directly
manipulating internal state (_spawn_lock, _kill, _worker_proven) rather
than via grade_codegen(). Delete the dead worker and the test still
passed. Rewrite to:
  - Grade once through the public API and assert the result.
  - Wait for the worker to exit naturally (returncode != None).
  - Grade again and assert both the result and that _proc changed.

Fixes thread #3708795700.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>

@ajcasagrande ajcasagrande left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

Found 2 issues, both inline below.

Minor: this PR changes the codegen grader's execution model without touching docs/ (CLAUDE.md L161-L166).

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Comment thread src/aiperf/accuracy/graders/_codegen_worker.py
Comment thread src/aiperf/accuracy/graders/_codegen_worker_client.py Outdated
debermudez and others added 3 commits August 4, 2026 16:13
…r-raises contract

handle_batch documents "Never raises" but the extend() calls on
all_samples/all_generations sat outside the (KeyError, TypeError)
guard. A request with evaluation_sample=null passes the dict lookups
(sample=None) then raises TypeError in extend(), propagating out of
handle_batch and run_worker_loop and killing the worker along with
every well-formed sibling in the batch.

Move start/extend into the try block so any TypeError from a null or
non-iterable field produces an error response instead.

Fixes thread #3716745952.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
…rkers

aclose() called _kill() without holding _spawn_lock. If a grade was
inside _ensure_worker() awaiting create_subprocess_exec, _kill() saw
_proc=None and skipped cleanup; the spawn then completed, assigned
self._proc, and left a start_new_session worker (plus any lighteval
sandbox grandchildren) alive with no owner.

Acquiring _spawn_lock before _kill() serialises teardown with any
in-flight spawn: either _kill() runs first and the spawn finds _proc
already cleared, or the spawn completes and _kill() then reaps it.

Fixes thread #3716745958.

Signed-off-by: Elias Bermudez <dbermudez@nvidia.com>
@ajcasagrande
ajcasagrande self-requested a review August 6, 2026 23:05

@ajcasagrande ajcasagrande left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. This is solid work — the demux-table design is the right architecture for restoring concurrency, and the implementation is careful about all the process-lifecycle and asyncio edge cases.

What's excellent:

  • id-based dispatch (not position-based) with stale-id tolerance
  • O_NONBLOCK batch drain — clever and verified correct for all edge cases
  • _spawn_lock in aclose() prevents orphaned workers (confirmed fixed)
  • _kill() always kills process group regardless of returncode (confirmed fixed)
  • self-await deadlock avoidance in _kill()
  • TimeoutError caught before OSError (PEP 3151 subclass trap)
  • Never-raises contract in handle_batch upheld
  • Cancellation preserves worker (unlike old code)
  • 57 unit tests pass, 10 targeted coverage-gap tests
  • Clean 27-commit history

One note (cosmetic): The _drain_buffered docstring is slightly imprecise about the O_NONBLOCK mechanism — see inline comments.

Full review: artifacts/code-review-20260807-final.md
Runtime receipts: artifacts/repro-runtime-20260807/

🤖 Generated with Claude Code

Comment thread src/aiperf/accuracy/graders/_codegen_worker.py
Comment thread src/aiperf/accuracy/graders/_codegen_worker.py
@ajcasagrande
ajcasagrande self-requested a review August 7, 2026 05:12
@debermudez
debermudez enabled auto-merge (squash) August 7, 2026 20:44
@debermudez
debermudez merged commit 03c9c6d into main Aug 7, 2026
28 checks passed
@debermudez
debermudez deleted the dbermudez/aip-1094-restore-codegen-grade-concurrency branch August 7, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants